// SPDX-License-Identifier: GPL-2.0
/*
 * -*- mode: c; c-basic-offset: 8; -*-
 * vim: noexpandtab sw=8 ts=8 sts=0:
 *
 * Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/pagemap.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/highmem.h>
#include <linux/init.h>
#include <linux/string.h>
#include <linux/backing-dev.h>
#include <linux/poll.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/sched/task.h>
#include <linux/gfp.h>

#include "cluster/masklog.h"
#include "ocfs2_qos.h"

#define O2QOS_TOKEN_REFRESH_TIME_MS		1000
#define O2QOS_GET_TOKEN_RETRY_TIME_MS	200
#define O2QOS_TIMEOUT				    (120 * 1000)
#define O2QOS_MEGABYTE_TO_BYTE			20
#define O2QOS_SEMAPHORE_VALUE			8

#define UNMAP_OFF                       0
#define UNMAP_ON                        1

static unsigned long long o2qos_get_token_capacity(struct ocfs2_super *osb,
		struct o2qos_token_region *reg)
{
	struct ocfs2_dinode *super_di = NULL;
	uint64_t unmap_qos_capacity = 0;
	unsigned long long token_capacity = 0;

	mutex_lock(&osb->system_file_mutex);
	if (osb->osb_super_bh == NULL) {
		mlog(ML_RO,
			"Failed to read token_capacity, using former value %llu.\n",
			reg->token_capacity);
		mutex_unlock(&osb->system_file_mutex);
		return reg->token_capacity;
	}

	super_di = (struct ocfs2_dinode *) osb->osb_super_bh->b_data;
	unmap_qos_capacity = le64_to_cpu(super_di->id2.i_super.unmap_qos_capacity);
	token_capacity = unmap_qos_capacity << O2QOS_MEGABYTE_TO_BYTE;

	mutex_unlock(&osb->system_file_mutex);

	return token_capacity;
}

static unsigned long o2qos_get_discard_speed(struct ocfs2_super *osb,
		struct o2qos_token_region *reg)
{
	struct ocfs2_dinode *super_di = NULL;
	uint64_t unmap_reclaim_unit = 0;
	unsigned long token_speed_limit = 0;

	mutex_lock(&osb->system_file_mutex);
	if (osb->osb_super_bh == NULL) {
		mlog(ML_RO,
			"Failed to read token_speed_limit, using former value %lu.\n",
			reg->token_speed_limit);
		mutex_unlock(&osb->system_file_mutex);
		return reg->token_speed_limit;
	}

	super_di = (struct ocfs2_dinode *) osb->osb_super_bh->b_data;
	unmap_reclaim_unit = le64_to_cpu(super_di->id2.i_super.unmap_reclaim_unit);
	token_speed_limit = unmap_reclaim_unit << O2QOS_MEGABYTE_TO_BYTE;

	mutex_unlock(&osb->system_file_mutex);

	return token_speed_limit;
}

void init_token_region(struct super_block *sb, struct o2qos_token_region *reg)
{
	struct ocfs2_super *osb = OCFS2_SB(sb);

	spin_lock_init(&reg->lock);
	reg->qos_generation = osb->fs_generation;
	reg->token_task = NULL;
	sema_init(&reg->sem, O2QOS_SEMAPHORE_VALUE);
	reg->token_capacity = o2qos_get_token_capacity(osb, reg);
	reg->token_period = O2QOS_TOKEN_REFRESH_TIME_MS;
	reg->token_speed_limit = o2qos_get_discard_speed(osb, reg);
	atomic64_set(&reg->token_available, reg->token_capacity);
}

static int o2qos_get_token(struct o2qos_token_region *reg, int size, uint64_t quest_id)
{
	int ret = 0;

	spin_lock(&reg->lock);
	if (size > atomic64_read(&reg->token_available)) {
		ret = -RETRY;
	} else {
		mlog(ML_RO, "Quest Id:%llx, token is enough, qos is ok.\n",
			(unsigned long long) le64_to_cpu(quest_id));
		atomic64_add(-size, &reg->token_available);
		ret = 0;
	}
	spin_unlock(&reg->lock);
	return ret;
}

int ocfs2_get_qos_token(struct super_block *sb, int size, unsigned int timeoutMs)
{
	unsigned long long quest_id = 0;
	unsigned long before = 0;
	int ret = 0;
	struct ocfs2_super *osb = OCFS2_SB(sb);
	struct o2qos_token_region *reg = osb->o2qos_token_region;

	if (!reg || !reg->token_task) {
		mlog(ML_ERROR, "%s: QoS module is not initialized.\n", osb->uuid_str);
		return -ENQOS;
	}

	quest_id = 0;
	get_random_bytes(&quest_id, sizeof(quest_id));

	if (size < 0 || size > reg->token_capacity) {
		mlog(ML_ERROR, "%s: Quest Id:%llx,illegal IO size.\n",
				osb->uuid_str, (unsigned long long) le64_to_cpu(quest_id));
		return -ESIZE;
	}

	ret = down_timeout(&reg->sem, O2QOS_TIMEOUT);
	if (ret < 0) {
		mlog(ML_ERROR,
				"%s: Quest Id:%llx,timeout when getting semaphore.\n",
				osb->uuid_str,
				(unsigned long long) le64_to_cpu(quest_id));
		return ret;
	}

	before = jiffies;
	do {
		if (!o2qos_get_token(reg, size, quest_id)) {
			up(&reg->sem);
			return 0;
		}
		msleep_interruptible(O2QOS_GET_TOKEN_RETRY_TIME_MS);
	} while (jiffies_to_msecs(jiffies - before) < timeoutMs);
	mlog(ML_RO, "%s: Token size %d is not enough after tring %u ms.\n",
		osb->uuid_str, size, timeoutMs);
	up(&reg->sem);
	return -ETOUT;
}
EXPORT_SYMBOL_GPL(ocfs2_get_qos_token);

int ocfs2_check_unmap_available(struct ocfs2_super *osb)
{
	struct o2qos_token_region *reg = osb->o2qos_token_region;
	unsigned long token_speed_limit = 0;
	unsigned long long token_capacity = 0;

	if (!reg || !reg->token_task) {
		mlog(ML_ERROR, "%s: QoS module is not initialized.\n", osb->uuid_str);
		return UNMAP_OFF;
	}

	token_speed_limit = o2qos_get_discard_speed(osb, reg);
	if (token_speed_limit == 0) {
		mlog(ML_RO, "%s: Unmap is shutted down.\n", osb->uuid_str);
		return UNMAP_OFF;
	}

	spin_lock(&reg->lock);
	if (token_speed_limit != reg->token_speed_limit) {
		reg->token_speed_limit = token_speed_limit;
		mlog(ML_RO, "%s: Update unmap speed limit value to %lu.\n",
			osb->uuid_str, token_speed_limit);
	}

	token_capacity = o2qos_get_token_capacity(osb, reg);
	if (token_capacity != reg->token_capacity) {
		reg->token_capacity = token_capacity;
		mlog(ML_RO, "%s: Update unmap token capacity to %llu.\n",
			osb->uuid_str, token_capacity);
		if (atomic64_read(&reg->token_available) > reg->token_capacity)
			atomic64_set(&reg->token_available, reg->token_capacity);
	}
	spin_unlock(&reg->lock);

	return UNMAP_ON;
}
EXPORT_SYMBOL_GPL(ocfs2_check_unmap_available);

int o2qos_token_thread(void *data)
{
	struct o2qos_token_region *reg = data;

	set_user_nice(current, MIN_NICE);
	while (!kthread_should_stop()) {
		if (atomic64_read(&reg->token_available) < reg->token_capacity) {
			spin_lock(&reg->lock);
			if ((reg->token_capacity - atomic64_read(&reg->token_available))
				> reg->token_speed_limit)
				atomic64_add(reg->token_speed_limit, &reg->token_available);
			else
				atomic64_set(&reg->token_available, reg->token_capacity);
			mlog(ML_DEBUG, "Set token to %llu.\n",
					atomic64_read(&reg->token_available));
			spin_unlock(&reg->lock);
		}
		msleep_interruptible(reg->token_period);
	}
	return 0;
}

int init_o2qos(struct super_block *sb)
{
	struct ocfs2_super *osb = OCFS2_SB(sb);
	struct o2qos_token_region *reg = NULL;
	struct task_struct *qos_task = NULL;
	int ret = 0;

	mlog(ML_NOTICE, "%s: o2qos initing,generation: %u.\n", osb->uuid_str, osb->fs_generation);
	reg = kzalloc(sizeof(struct o2qos_token_region), GFP_KERNEL);
	if (reg == NULL) {
		mlog(ML_ERROR, "alloc o2qos_token_region failed.\n");
		return -ENOMEM;
	}

	init_token_region(sb, reg);

	osb->o2qos_token_region = reg;

	qos_task = kthread_create(o2qos_token_thread, reg, "o2qos-%lu", reg->qos_generation);
	if (IS_ERR(qos_task)) {
		ret = PTR_ERR(qos_task);
		osb->o2qos_token_region = NULL;
		kfree(reg);
		mlog_errno(ret);
		return ret;
	}
	/* get reference to prevent from NULL pointer dereference
	 * when umount thread stop hr_task */
	get_task_struct(qos_task);
	wake_up_process(qos_task);
	reg->token_task = qos_task;
	return ret;
}
EXPORT_SYMBOL_GPL(init_o2qos);

void exit_o2qos(struct ocfs2_super *osb)
{
	struct task_struct *qos_task = NULL;
	struct o2qos_token_region *reg = osb->o2qos_token_region;

	osb->o2qos_token_region = NULL;

	if (reg) {
		mlog(ML_NOTICE, "%s: o2qos exiting,generation:%lu.\n",
			osb->uuid_str, reg->qos_generation);
		qos_task = reg->token_task;
		reg->token_task = NULL;
		if (qos_task) {
			kthread_stop(qos_task);
			put_task_struct(qos_task);
		}
		kfree(reg);
	}
}
EXPORT_SYMBOL_GPL(exit_o2qos);

static int __init ocfs2_qos_init(void)
{
	printk(KERN_INFO "o2qos module init\n");
	return 0;
}

static void __exit ocfs2_qos_exit(void)
{
	printk(KERN_INFO "o2qos module exit\n");
}

module_init(ocfs2_qos_init);
module_exit(ocfs2_qos_exit);

MODULE_AUTHOR("Huawei");
MODULE_LICENSE("GPL");

